﻿namespace NJQuery.BlockUIExtension {
	using System;
	using System.ComponentModel;
	using System.Globalization;
	using System.Web.UI;
	using System.Text;
	using System.Diagnostics.CodeAnalysis;

	/// <summary>
	/// Server control used to reference the <see href="http://www.malsup.com/jquery/block/">blockUI widget</see> and define default page settings for further client-side use.
	/// </summary>
	[ParseChildren(true), PersistChildren(false)]
	public class BlockUI : ManagedControl {


		#region Constants: Initialization option defaults

		private const bool defaultTheme = false;
		private const string defaultMessage = "<h1>Please wait...</h1>";
		private const bool defaultDraggable = false;
		private const int defaultBaseZ = 1000;
		private const bool defaultCenterX = true;
		private const bool defaultCenterY = true;
		private const bool defaultAllowBodyStretch = true;
		private const bool defaultConstrainTabKey = true;
		private const int defaultFadeIn = 200;
		private const int defaultFadeOut = 400;
		private const int defaultTimeout = 0;
		private const bool defaultShowOverlay = true;
		private const bool defaultFocusInput = true;
		private const bool defaultApplyPlatformOpacityRules = true;

		#endregion


		#region Constants: Other defaults

		private const bool defaultDisplayOnPostBack = false;

		#endregion


		#region Static Methods: Protected helpers

		/// <summary>
		/// Sets the <see cref="BlockUI"/> into the page's items dictionary.
		/// </summary>
		/// <param name="page">The <see cref="Page"/> the rendering control resides within.</param>
		/// <param name="value">The jQuery <see cref="BlockUI"/> control to place into the items dictionary.</param>
		protected static void Set(Page page, BlockUI value) {
			if(page == null) throw new ArgumentNullException("page");
			if(value == null) throw new ArgumentNullException("value");

			page.Items[typeof(BlockUI)] = value;
		}

		#endregion


		#region Static Methods: Public

		/// <summary>
		/// Returns the single instance of the <see cref="BlockUI"/> control if present.
		/// </summary>
		/// <param name="page">The <see cref="Page"/> the rendering control resides within.</param>
		/// <returns>The instance of the jQuery <see cref="BlockUI"/> control if present, otherwise null.</returns>
		/// <exception cref="ArgumentNullException">A page reference is required.</exception>
		public static BlockUI GetCurrent(Page page) {
			if(page == null) throw new ArgumentNullException("page");

			return page.Items[typeof(BlockUI)] as BlockUI;
		}

		#endregion


		#region Fields

		private ITemplate message;
		private GroupControlContainer messageContainer;
		private ITemplate title;
		private GroupControlContainer titleContainer;

		#endregion


		#region Constructors

		/// <summary>
		/// Initializes a new instance of the <see cerf="BlockUI"/> class.
		/// </summary>
		public BlockUI() { }

		#endregion


		#region Properties: Initialization options

		/// <summary>
		/// Gets or sets the HTML content displayed when blocking.
		/// This property maps directly to the "message" default value.
		/// </summary>
		[TemplateInstance(TemplateInstance.Single), PersistenceMode(PersistenceMode.InnerProperty), Browsable(false)]
		public virtual ITemplate Message {
			get { return message; }
			set { message = value; }
		}

		/// <summary>
		/// Gets a control object to which you can programmatically add child controls for <see cref="Message"/>.
		/// </summary>
		[Browsable(false)]
		public Control MessageContainer {
			get {
				EnsureChildControls();
				return messageContainer;
			}
		}

		[TemplateInstance(TemplateInstance.Single), PersistenceMode(PersistenceMode.InnerProperty), Browsable(false)]
		public virtual ITemplate Title {
			get { return title; }
			set { title = value; }
		}

		/// <summary>
		/// Gets a control object to which you can programmatically add child controls for <see cref="Title"/>.
		/// </summary>
		[Browsable(false)]
		public Control TitleContainer {
			get {
				EnsureChildControls();
				return titleContainer;
			}
		}

		[DefaultValue(defaultTheme), Category("Appearance")]
		public virtual bool Theme {
			get { return ViewState["Theme"] as bool? ?? defaultTheme; }
			set { ViewState["Theme"] = value; }
		}

		[DefaultValue(defaultDraggable), Category("Behavior")]
		public virtual bool Draggable {
			get { return ViewState["Draggable"] as bool? ?? defaultDraggable; }
			set { ViewState["Draggable"] = value; }
		}

		/// <summary>
		/// Gets or sets the z-index for the blocking overlay.
		/// This property maps directly to the "baseZ" default value.
		/// </summary>
		[DefaultValue(defaultBaseZ), Category("Layout")] //TODO: Is Layout correct for a z-order?
		public virtual int BaseZ {
			get { return ViewState["BaseZ"] as int? ?? defaultBaseZ; }
			set { ViewState["BaseZ"] = value; }
		}

		/// <summary>
		/// Gets or sets whether the message automatically centered horizontally.  This only affects element blocking, as page blocking is handled through CSS.
		/// This property maps directly to the "centerX" default value.
		/// </summary>
		[DefaultValue(defaultCenterX), Category("Appearance")]
		public virtual bool CenterX {
			get { return ViewState["CenterX"] as bool? ?? defaultCenterX; }
			set { ViewState["CenterX"] = value; }
		}

		/// <summary>
		/// Gets or sets whether the message automatically centered vertically.
		/// This property maps directly to the "centerY" default value.
		/// </summary>
		[DefaultValue(defaultCenterY), Category("Appearance")]
		public virtual bool CenterY {
			get { return ViewState["CenterY"] as bool? ?? defaultCenterY; }
			set { ViewState["CenterY"] = value; }
		}

		/// <summary>
		/// Gets or sets whether to allow body element to be stetched in IE6 to make blocking look better on short pages.  Set to false if you wish to prevent changes to the body height.
		/// This property maps directly to the "allowBodyStretch" default value.
		/// </summary>
		[DefaultValue(defaultAllowBodyStretch), Category("Behavior")]
		public virtual bool AllowBodyStretch {
			get { return ViewState["AllowBodyStretch"] as bool? ?? defaultAllowBodyStretch; }
			set { ViewState["AllowBodyStretch"] = value; }
		}

		/// <summary>
		/// Gets or sets whether blockUI will supress tab navigation from leaving blocking content.
		/// This property maps directly to the "constrainTabKey" default value.
		/// </summary>
		[DefaultValue(defaultConstrainTabKey), Category("Behavior")]
		public virtual bool ConstrainTabKey {
			get { return ViewState["ConstrainTabKey"] as bool? ?? defaultConstrainTabKey; }
			set { ViewState["ConstrainTabKey"] = value; }
		}

		/// <summary>
		/// Gets or sets the fade in time (in milliseconds).  Set to 0 to disable fade in on block.
		/// This property maps directly to the "fadeIn" default value.
		/// </summary>
		/// <exception cref="ArgumentOutOfRangeException">Value cannot be negative.</exception>
		[DefaultValue(defaultFadeIn), Category("Appearance")]
		public virtual int FadeIn {
			get { return ViewState["FadeIn"] as int? ?? defaultFadeIn; }
			set {
				if(value < 0) throw new ArgumentOutOfRangeException("value", "Value cannot be negative.");
				ViewState["FadeIn"] = value;
			}
		}

		/// <summary>
		/// Gets or sets the fade out time (in milliseconds).  Set to 0 to disable fade out on unblock.
		/// This property maps directly to the "fadeOut" default value.
		/// </summary>
		/// <exception cref="ArgumentOutOfRangeException">Value cannot be negative.</exception>
		[DefaultValue(defaultFadeOut), Category("Appearance")]
		[SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "FadeOut", Justification = "The word is not fadeout.  It's fade in/fade out")]
		public virtual int FadeOut {
			get { return ViewState["FadeOut"] as int? ?? defaultFadeOut; }
			set {
				if(value < 0) throw new ArgumentOutOfRangeException("value", "Value cannot be negative.");
				ViewState["FadeOut"] = value;
			}
		}

		/// <summary>
		/// Gets or sets the time (in milliseconds) to wait before auto-unblocking.  Set to 0 to disable auto-unblock.
		/// This property maps directly to the "timeout" default value.
		/// </summary>
		/// <exception cref="ArgumentOutOfRangeException">Value cannot be negative.</exception>
		[DefaultValue(defaultTimeout), Category("Appearance")]
		public virtual int Timeout {
			get { return ViewState["Timeout"] as int? ?? defaultTimeout; }
			set {
				if(value < 0) throw new ArgumentOutOfRangeException("value", "Value cannot be negative.");
				ViewState["Timeout"] = value;
			}
		}

		/// <summary>
		/// Gets or set whether or not to show a visual overlay.
		/// This property maps directly to the "showOverlay" default value.
		/// </summary>
		[DefaultValue(defaultShowOverlay), Category("Appearance")]
		public virtual bool ShowOverlay {
			get { return ViewState["ShowOverlay"] as bool? ?? defaultShowOverlay; }
			set { ViewState["ShowOverlay"] = value; }
		}

		/// <summary>
		/// Gets or sets whether or not focus will be placed in the first available input field when page blocking.
		/// This property maps directly to the "focusInput" default value.
		/// </summary>
		[DefaultValue(defaultFocusInput), Category("Behavior")]
		public virtual bool FocusInput {
			get { return ViewState["FocusInput"] as bool? ?? defaultFocusInput; }
			set { ViewState["FocusInput"] = value; }
		}

		/// <summary>
		/// Gets or sets whether or not to use overlay styles on Firefox for Linux due to performance issues with opacity.
		/// This property maps directly to the "applyPlatformOpacityRules" default value.
		/// </summary>
		[DefaultValue(defaultApplyPlatformOpacityRules), Category("Behavior")]
		public virtual bool ApplyPlatformOpacityRules {
			get { return ViewState["ApplyPlatformOpacityRules"] as bool? ?? defaultApplyPlatformOpacityRules; }
			set { ViewState["ApplyPlatformOpacityRules"] = value; }
		}

		#endregion


		#region Properties: Additional

		/// <summary>
		/// Gets or sets whether the control displays automatically when the page performs a postback.
		/// </summary>
		[DefaultValue(defaultDisplayOnPostBack), Category("Behavior")]
		public virtual bool DisplayOnPostBack {
			get { return ViewState["DisplayOnPostBack"] as bool? ?? false; }
			set { ViewState["DisplayOnPostBack"] = value; }
		}

		#endregion


		#region Methods: Child controls

		/// <summary>
		/// Called by the ASP.NET page framework to notify server controls that use composition-based implementation to create any child controls they contain in preparation for posting back or rendering.
		/// </summary>
		protected override void CreateChildControls() {
			base.CreateChildControls();

			if(messageContainer == null) {
				messageContainer = CreateMessageContainer();
				messageContainer.Visible = false;
				GroupedControls.AddContainer(messageContainer);
			}

			if(titleContainer == null) {
				titleContainer = CreateTitleContainer();
				titleContainer.Visible = false;
				GroupedControls.AddContainer(titleContainer);
			}

		}

		/// <summary>
		/// Creates the control object to which the user can programmatically add child controls for <see cref="Message"/>.
		/// </summary>
		/// <returns>The <see cref="GroupControlContainer"/> to group the child controls.</returns>
		protected virtual GroupControlContainer CreateMessageContainer() {
			return new GroupControlContainer(this);
		}

		/// <summary>
		/// Creates the control object to which the user can programmatically add child controls for <see cref="Title"/>.
		/// </summary>
		/// <returns>The <see cref="GroupControlContainer"/> to group the child controls.</returns>
		protected virtual GroupControlContainer CreateTitleContainer() {
			return new GroupControlContainer(this);
		}

		#endregion


		#region Methods: Render hooks

		/// <summary>
		/// Raises the <see cref="Control.Init"/> event.
		/// </summary>
		/// <param name="e">The <see cref="EventArgs"/> object that contains the event data.</param>
		protected override void OnInit(EventArgs e) {
			base.OnInit(e);

			if(Message == null)
				MessageContainer.Controls.Add(new LiteralControl(defaultMessage));
			else
				Message.InstantiateIn(MessageContainer);

			if(Title != null) Title.InstantiateIn(TitleContainer);

			if(DesignMode) return;

			if(GetCurrent(Page) != null) throw new InvalidOperationException("Page can contain only one instance of " + typeof(BlockUI).FullName + ".");
			Set(Page, this);
		}

		/// <summary>
		/// Allows a jQuery server control to reference scripts and stylesheets necessary to render their client markup and widgets.
		/// </summary>
		/// <param name="manager">The <see cref="Manager"/> used to handle style and script referencing in addition to unified generation of load and ready scripts.</param>
		protected override void Register(Manager manager) {

			manager.RegisterClientScript(NJQuery.ClientScript.Core);
			if(Draggable) {
				manager.RegisterClientScript(NJQuery.ClientScript.UICore);
				manager.RegisterClientScript(NJQuery.ClientScript.UIWidgetFactory);
				manager.RegisterClientScript(NJQuery.ClientScript.UIMouseUtility);
				manager.RegisterClientScript(NJQuery.ClientScript.UIDraggableInteraction);
			}
			manager.RegisterClientScript(ClientScript.BlockUIWidget);

			if(Theme) {
				manager.RegisterClientStyle(ClientStyle.UICore);
				manager.RegisterClientStyle(ClientStyle.UIDialogWidget);
				manager.RegisterTheme();
			}

			manager.RegisterForReadyScript(this);

			bool displayOnPostBack = DisplayOnPostBack;
			if(displayOnPostBack) manager.RegisterForSubmitScript(this);
			if(displayOnPostBack && Page.IsPostBack) manager.RegisterForStartupScript(this); //TODO: CHange to a hidden field rather than assuming

		}

		/// <summary>
		/// Renders the jQuery "Ready Event" script required to generate the client-side jQuery widget.
		/// This method is used primarily by control developers.
		/// </summary>
		/// <param name="script">A <see cref="ScriptBuilder"/> that creates the output stream to render jQuery JavaScript on the client.</param>
		protected override void RenderReadyScript(ScriptBuilder script) {

			//TODO: Forget about the defaults.  I think all widgets have them anyway.  We need to set this on block.

			//bool masterUseCssStyling = MasterUseCssStyling;

			//if(masterUseCssStyling) {
			//	RenderDefaultValue(script, "css", () => script.Append("{}"));
			//	RenderDefaultValue(script, "themedCSS", () => script.Append("{}"));
			//	RenderDefaultValue(script, "overlayCSS", () => script.Append("{}"));
			//	RenderDefaultValue(script, "growlCSS", () => script.Append("{}"));
			//}

			bool theme = Theme;

			string message = messageContainer.RenderHtml();
			if(message != defaultMessage) RenderDefaultValue(script, "message", () => script.AppendValue(message));

			if(theme) {
				string title = titleContainer.RenderHtml() ?? string.Empty;
				if(title.Length != 0) RenderDefaultValue(script, "title", () => script.AppendValue(title));
			}

			bool draggable = Draggable;
			if(draggable != defaultDraggable) RenderDefaultValue(script, "draggable", () => script.AppendValue(draggable));

			if(theme != defaultTheme) RenderDefaultValue(script, "theme", () => script.AppendValue(theme));

			int baseZ = BaseZ;
			if(baseZ != defaultBaseZ) RenderDefaultValue(script, "baseZ", () => script.AppendValue(baseZ));

			bool centerX = CenterX;
			if(centerX != defaultCenterX) RenderDefaultValue(script, "centerX", () => script.AppendValue(centerX));

			bool centerY = CenterY;
			if(centerY != defaultCenterY) RenderDefaultValue(script, "centerY", () => script.AppendValue(centerY));

			bool allowBodyStretch = AllowBodyStretch;
			if(allowBodyStretch != defaultAllowBodyStretch) RenderDefaultValue(script, "allowBodyStretch", () => script.AppendValue(allowBodyStretch));

			int fadeIn = FadeIn;
			if(fadeIn != defaultFadeIn) RenderDefaultValue(script, "fadeIn", () => script.AppendValue(fadeIn));

			int fadeOut = FadeOut;
			if(fadeOut != defaultFadeOut) RenderDefaultValue(script, "fadeOut", () => script.AppendValue(fadeOut));

			int timeout = Timeout;
			if(timeout != defaultTimeout) RenderDefaultValue(script, "timeout", () => script.AppendValue(timeout));

			bool showOverlay = ShowOverlay;
			if(showOverlay != defaultShowOverlay) RenderDefaultValue(script, "showOverlay", () => script.AppendValue(showOverlay));

			bool focusInput = FocusInput;
			if(focusInput != defaultFocusInput) RenderDefaultValue(script, "focusInput", () => script.AppendValue(focusInput));

			bool applyPlatformOpacityRules = ApplyPlatformOpacityRules;
			if(applyPlatformOpacityRules != defaultApplyPlatformOpacityRules) RenderDefaultValue(script, "applyPlatformOpacityRules", () => script.AppendValue(applyPlatformOpacityRules));

		}

		protected override void RenderStartupScript(ScriptBuilder script) {
			script.NewLine();
			script.Append("$.unblockUI();");
		}

		/// <summary>
		/// Renders a script to be executed on form/postback submit.
		/// This method is used primarily by control developers.
		/// </summary>
		/// <param name="script">A <see cref="ScriptBuilder"/> that creates the output stream to render jQuery JavaScript on the client.</param>
		protected override void RenderSubmitScript(ScriptBuilder script) {
			script.NewLine();
			script.Append("$.blockUI();");
		}

		/// <summary>
		/// A delegate required to render the actual literal value to the client when calling <see cref="RenderDefaultValue(ScriptBuilder,string,AppendValue)"/>
		/// </summary>
		protected delegate void AppendValue();

		/// <summary>
		/// Renders the client-side jQuery assignation of a default setting.
		/// </summary>
		/// <param name="script">A <see cref="ScriptBuilder"/> that creates the output stream to render jQuery JavaScript on the client.</param>
		/// <param name="name">The name of the variable to initialize.</param>
		/// <param name="appendValue">A delegate required to render the actual literal value to the client.</param>
		protected virtual void RenderDefaultValue(ScriptBuilder script, string name, AppendValue appendValue) {
			if(script == null) throw new ArgumentNullException("script");
			if(name == null) throw new ArgumentNullException("name");
			if(name.Length == 0) throw new ArgumentException("A value is required.", "name");
			if(appendValue == null) throw new ArgumentNullException("appendValue");

			script.NewLine();

			script.Append("$.blockUI.defaults.");
			script.Append(name);
			script.Append("=");
			appendValue();
			script.Append(";");
		}

		#endregion


	}

}